Flutter

您所在的位置:网站首页 flutter tabview 状态 Flutter

Flutter

2023-11-04 09:35| 来源: 网络整理| 查看: 265

NestedScrollView

CustomScrollView只能组合Sliver,如果有子组件也是一个可滚动(通过SliverToBoxAdapter嵌入)且它们的滑动方向一致时便不能工作。为了解决这个问题,Flutter提供了NestedScrollView组件,它的功能是协调两个可滚动组件。

const NestedScrollView({ ... //省略可滚动组件的通用属性 //header,sliver构造器 required this.headerSliverBuilder, //可以接受任意的可滚动组件 required this.body, this.floatHeaderSlivers = false, })

image.png

上面效果有三个部分组成:

最上面的一个AppBar,实现导航,需要固定在顶部。 AppBar下面是一个SliverList,可以有任意多个列表项 最下面的ListView。

预期效果是SliverList和下面的ListView的滑动能够统一,而不是下面ListView上滑动时只有ListView响应滑动,整个页面在垂直方向是一个整体。

Material( child: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { // 返回一个 Sliver 数组给外部可滚动组件。 return [ SliverAppBar( title: const Text('嵌套ListView'), pinned: true, // 固定在顶部 forceElevated: innerBoxIsScrolled, ), buildSliverList(5), //构建一个 sliverList ]; }, body: ListView.builder( padding: const EdgeInsets.all(8), physics: const ClampingScrollPhysics(), //重要 itemCount: 30, itemBuilder: (BuildContext context, int index) { return SizedBox( height: 50, child: Center(child: Text('Item $index')), ); }, ), ), );

NestedScrollView在逻辑上将可滚动组件分为了header和body两部分,header部分可以认为外部可滚动组件(outer scroll view),可以认为这个可滚动组件就是CustomScrollView,所以它只能接收Sliver,通过headerSliverBuilder来构建一个Sliver列表给外部的可滚动组件;而body部分可以接收任意的可滚动组件,该可滚动组件称为内部可滚动组件(inner scroll view)。

NestedScrollView原理

image.png

NestedScrollView整体就是一个CunstomScrollView,实际上继承自CustomScrollView。 header和body都是CustomScrollView的子Sliver,注意,虽然body是一个RenderBox,但是会被包装为Sliver。 CustomScrollView将所有子Sliver在逻辑上分为header和body两部分,header是前部分,body是后部分。 当body是一个可滚动组件时,它和CustomScrollView分别有一个Scrollable,由于body在CustomScrollView的内部,所以称其为内部可滚动组件,称header为外部可滚动组件。同时因为header部分是Sliver,所以没有独立的Scrollable,滑动时受CustomScrollView的Scrollable控制。 NestedScrollView核心功能就是通过一个协调器来协调外部(outer)可滚动组件和内部(inner)可滚动组件的滚动,以便使滑动效果连贯统一。协调器的实现原理就是分别给内外可滚动组件分别设置一个controller,然后通过这两个controller来协调控制它们的滚动。

综上:

要确认内部的可滚动组件(body)的physics是否需要设置为ClampingScrollPhysics。比如,当ListView没有设置为ClampingScrollPhysics,则用户快速滑动到顶部时,会执行一个弹性效果,此时ListView就会与header显得割裂(滑动效果不统一)。所以需要设置。但是如果header中只有一个SliverAppBar则不应该加,因为SliverAppBar是固定在顶部的,ListView滑动到顶部时上面已经没有要继续往下滑动的元素来,所以此时出现弹性效果是符合预期的。 内部的可滚动组件(body)不能设置controller和primary,这是因为NestedScrollView的协调器中已经指定来它的Controller,如果重新设定则协调器会失效。 SliverAppBar

SliverAppBar是AppBar的Sliver版,但多数的参数相同,但是SliverAppBar有一些特有的功能:

const SliverAppBar({ this.collapsedHeight, // 收缩起来的高度 this.expandedHeight,// 展开时的高度 this.pinned = false, // 是否固定 this.floating = false, //是否漂浮 this.snap = false, // 当漂浮时,此参数才有效 bool forceElevated //导航栏下面是否一直显示阴影 ... }) SliverAppBar在NestedScrollView中随着用户的滑动可以收缩和展开,因此需要分别指定收缩和展开时的高度。 pinned为True时SliverAppBar会固定在NestedScrollView的顶部,行为和SliverPersistentHeader的pinned功能一致。 floating和snap:floating为true时,SliverAppbar不会固定到顶部,当用户向上滑动到顶部时,SliverAppbar也会滑出可视窗口。当用户反向滑动时,SliverAppBar的snap为true时,此时无论SliverAppbar已经滑出屏幕多远,都会立即回到屏幕顶部;但是snap为false,则SliverAppBar只有当向下滑到边界时才会重新回到屏幕顶部。这一点和SliverPersistentHeader的floating相似,但不同的是SliverPersistentHeader没有snap参数,当它的floating为true时,效果时等同于SliverAppBar的floating和snap同时为true时效果。

SliverAppBar的一些参数和SliverPersistentHeader很像,因为SliverAppBar内部包含一个SliverPersistentHeader,用于实现顶部固定和漂浮效果。

class SnapAppBar extends StatelessWidget { const SnapAppBar({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ //如果此处不使用SliverOverlapAbsorber头部会有部分列表被遮挡 SliverOverlapAbsorber( //传递重叠长度 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( floating: true, snap: true, expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( background: Image.asset( "./imgs/sea.png", fit: BoxFit.cover, ), ), forceElevated: innerBoxIsScrolled, ), ), ]; }, body: Builder(builder: (BuildContext context) { return CustomScrollView( slivers: [ SliverOverlapInjector( //传递重叠长度 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), buildSliverList(100) ], ); }), ), ); } }

注意:

SliverAppBar用SliverOverlapAbsorber包裹起来的作用是,获取SliverAppbar返回是遮住内部可滚动组件的部分长度,这个长度就是overlap(重叠)的长度。 在body中往CustomScrollView的Sliver列表的最前面插入了一个SliverOverlapInjector,它会将SliverOVerlapAbsorber中获取的overlap长度应用到内部可滚动组件中。这样在SliverAppBar返回时,内部可滚动组件也会相应的同步滑动相应的距离。

SliverOverlapAbsorber和SliverOverlapInjector都接收一个handle,给它传入的是NestedScrollView.sliverOverlapAbsorberHandleFor(context)。handle就是SliverOverlapAbsorber和SliverOverlapInjector的通信桥梁,即传递overlap长度。

当snap为true时,只需要给SliverAppBar包裹一个SliverOverlapAbsorber即可,而无需再给CustomScrollView添加SliverOverlapinjector,因为这种情况SliverOverlapAbsorber会自动吸收Overlap,以调整自身的布局高度为SliverAppBar的实际高度,这样的话header的高度变化后就会自动将body向下撑(header和body属于同一个CustomScrollView),同时handle中的overlap长度始终0。而只有当SliverAppBar被SliverOverlapAbsorber包裹且为固定模式时(pinned为true),CustomScrollView中添加SliverOverlapInjector才有意义,handle中的overlap长度不为0。

验证:

class SnapAppBar2 extends StatefulWidget { const SnapAppBar2({Key? key}) : super(key: key); @override State createState() => _SnapAppBar2State(); } class _SnapAppBar2State extends State { // 将handle 缓存 late SliverOverlapAbsorberHandle handle; void onOverlapChanged(){ // 打印 overlap length print(handle.layoutExtent); } @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { handle = NestedScrollView.sliverOverlapAbsorberHandleFor(context); //添加监听前先移除旧的 handle.removeListener(onOverlapChanged); //overlap长度发生变化时打印 handle.addListener(onOverlapChanged); return [ SliverOverlapAbsorber( handle: handle, sliver: SliverAppBar( floating: true, snap: true, // pinned: true, // 放开注释,然后看日志 expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( background: Image.asset( "./imgs/sea.png", fit: BoxFit.cover, ), ), forceElevated: innerBoxIsScrolled, ), ), ]; }, body: LayoutBuilder(builder: (BuildContext context,cons) { return CustomScrollView( slivers: [ SliverOverlapInjector(handle: handle), buildSliverList(100) ], ); }), ), ); } @override void dispose() { // 移除监听器 handle.removeListener(onOverlapChanged); super.dispose(); } }

分别查看snap和pinned模式下控制台的输出即可验证。

综上:建议SLiverOverlapAbsorber和SliverOverlapInjector配对使用,这样可以避免日后将snap模式改为固定模式后忘记添加SliverOverlapInjector而导致bug。

嵌套TabBarView

使用实例:

class NestedTabBarView1 extends StatelessWidget { const NestedTabBarView1({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final _tabs = ['猜你喜欢', '今日特价', '发现更多']; // 构建 tabBar return DefaultTabController( length: _tabs.length, // tab的数量. child: Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: const Text('商城'), floating: true, snap: true, forceElevated: innerBoxIsScrolled, bottom: TabBar( tabs: _tabs.map((String name) => Tab(text: name)).toList(), ), ), ), ]; }, body: TabBarView( children: _tabs.map((String name) { return Builder( builder: (BuildContext context) { return CustomScrollView( key: PageStorageKey(name), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SliverPadding( padding: const EdgeInsets.all(8.0), sliver: buildSliverList(50), ), ], ); }, ); }).toList(), ), ), ), ); } }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3